/*
 * MadHelix is a Java Swing-based GUI frontend for SoundHelix.
 * Copyright (C) 2017-2019, 2023 UltrasonicMadness
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 only,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.ultrasonicmadness.madhelix;

import java.io.File;
import java.io.IOException;
import java.util.Random;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.SwingUtilities;
import javax.xml.stream.XMLStreamException;
import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;
import org.ultrasonicmadness.madhelix.concurrency.SoundHelixThread;
import org.ultrasonicmadness.madhelix.dialogs.AboutBox;
import org.ultrasonicmadness.madhelix.dialogs.PrefsDialog;
import org.ultrasonicmadness.madhelix.models.HistoryTableModel;
import org.ultrasonicmadness.madhelix.utils.PathUtils;
import org.ultrasonicmadness.madhelix.utils.VersionUtils;

public class MadHelix extends JFrame {
    // SoundHelix
    private final File stylesDir = new File(PathUtils.getStylesDirPath()); // Styles directory
    private SoundHelixThread soundHelix = new SoundHelixThread(this,
            "MadHelix-SoundHelix");

    // Components
    private JPanel mainPanel = new JPanel();

    // Status labels (spaces included so that the layout works)
    private JLabel statusLabel = new JLabel(" ");
    private JLabel playingSongLabel = new JLabel(" ");
    private JLabel playingSongStyleLabel = new JLabel(" ");

    // Timer
    private JLabel timerLabel = new JLabel("00:00");
    private JLabel lengthLabel = new JLabel("00:00");
    private JSlider seekBar = new JSlider(0, 0, 0);

    // Labels and data entry fields
    private JLabel songNameLabel = new JLabel("Song name");
    private JTextField songNameEntry = new JTextField(24);
    private JLabel styleLabel = new JLabel("Style");
    private JComboBox<String> styleChoice = new JComboBox<>();

    // Icons
    private ImageIcon playIcon = new ImageIcon(this.getClass()
            .getResource("/icons/media-playback-start-symbolic.png"), "Play");

    private ImageIcon stopIcon = new ImageIcon(this.getClass()
            .getResource("/icons/media-playback-stop-symbolic.png"), "Stop");

    private ImageIcon skipBackIcon = new ImageIcon(this.getClass()
            .getResource("/icons/object-rotate-left-symbolic.png"), "Skip back");

    private ImageIcon skipForwardIcon = new ImageIcon(this.getClass()
            .getResource("/icons/object-rotate-right-symbolic.png"), "Skip forward");

    private ImageIcon prefsIcon = new ImageIcon(this.getClass()
            .getResource("/icons/preferences-system-symbolic.png"), "Preferences");

    private ImageIcon aboutIcon = new ImageIcon(this.getClass()
            .getResource("/icons/help-about-symbolic.png"), "About MadHelix");

    // Play control buttons
    private JButton playButton = new JButton(playIcon);
    private JButton stopButton = new JButton(stopIcon);
    private JButton skipBackButton = new JButton(skipBackIcon);
    private JButton skipForwardButton = new JButton(skipForwardIcon);

    // History
    private JScrollPane historyScrollPane = null;
    private JTable historyTable = null;
    private HistoryTableModel historyTableModel = null;

    public MadHelix() {
        initMadHelix();
    }

    private void initMadHelix() {
        addComponents();

        soundHelix.setDaemon(true);
        soundHelix.start();

        // Set up main window
        this.setTitle("MadHelix");
        this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        this.addWindowListener(new WindowListener() {
            @Override
            public void windowActivated(WindowEvent e) {}

            @Override
            public void windowClosed(WindowEvent e) {}

            @Override
            public void windowClosing(WindowEvent e) {
                soundHelix.stopSong();
            }

            @Override
            public void windowDeactivated(WindowEvent e) {}

            @Override
            public void windowDeiconified(WindowEvent e) {}

            @Override
            public void windowIconified(WindowEvent e) {}

            @Override
            public void windowOpened(WindowEvent e) {}
        });
        this.pack();
        this.setMinimumSize(new Dimension(220, 280));
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }

    private void determineStatus() {
        // Assume program is ready , then check for errors
        setStatus("Ready");

        // If no styles are found, disable the buttons and display an alert.
        if (stylesDir.list() == null || stylesDir.list().length == 0) {
            setStatus("No styles available.");
            setControlsEnabled(false);

            JOptionPane.showMessageDialog(null,
                "No .xml files found in the styles directory",
                "Error",
                JOptionPane.ERROR_MESSAGE
            );
        }
    }

    private void addComponents() {
        // Set panel layout
        mainPanel.setLayout(new GridBagLayout());

        // Set up constraints
        GridBagConstraints mainConstraints = new GridBagConstraints();
        mainConstraints.fill = GridBagConstraints.HORIZONTAL;
        mainConstraints.weightx = 1;

        JPanel optionButtonsPanel = initOptionButtonsPanel();
        JPanel statusPanel = initStatusPanel();
        JPanel timerPanel = initTimerPanel();
        JPanel inputsPanel = initInputsPanel();
        JPanel playControlsPanel = initPlayControlsPanel();
        JPanel historyPanel = initHistoryPanel();

        // Add panels
        mainConstraints.gridy = 0;
        mainPanel.add(optionButtonsPanel, mainConstraints);

        ++mainConstraints.gridy;
        mainPanel.add(statusPanel, mainConstraints);

        ++mainConstraints.gridy;
        mainPanel.add(inputsPanel, mainConstraints);

        ++mainConstraints.gridy;
        mainPanel.add(playControlsPanel, mainConstraints);

        ++mainConstraints.gridy;
        mainPanel.add(timerPanel, mainConstraints);

        ++mainConstraints.gridy;
        mainConstraints.weighty = 1;
        mainConstraints.fill = GridBagConstraints.BOTH;
        mainPanel.add(historyPanel, mainConstraints);

        this.add(mainPanel);
    }

    private JPanel initStatusPanel() {
        JPanel statusPanel = new JPanel();
        determineStatus();

        // Set up layout
        statusPanel.setLayout(new GridBagLayout());

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.insets = new Insets(2, 2, 2, 2);
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.weightx = 1;

        // Add the labels to the panel
        constraints.gridy = 0;
        statusPanel.add(playingSongLabel, constraints);

        constraints.gridy = 1;
        statusPanel.add(playingSongStyleLabel, constraints);

        return statusPanel;
    }

    // Set status to the three passed strings
    public void setStatus(String newStatus, String newSongPlaying,
            String newSongStyle) {

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                // Set all label text to the passed strings.
                statusLabel.setText(newStatus);
                playingSongLabel.setText(newSongPlaying);
                playingSongStyleLabel.setText(newSongStyle);
            }
        });
    }

    // Set status to the passed string. The others are reset to blank.
    public void setStatus(String newStatus) {
        setStatus(newStatus, " ", " ");
    }

    private JPanel initOptionButtonsPanel() {
        JPanel optionButtonsPanel = new JPanel();

        JButton prefsButton = new JButton(prefsIcon);
        JButton aboutButton = new JButton(aboutIcon);

        // Add tooltips to the buttons
        prefsButton.setToolTipText("Preferences");
        aboutButton.setToolTipText("About MadHelix");

        // Set up layout
        statusLabel.setFont(new Font("sans-serif", Font.BOLD, 18));

        optionButtonsPanel.setLayout(new GridBagLayout());

        prefsButton.addActionListener(ev -> {
            new PrefsDialog(this, soundHelix).setVisible(true);
        });

        aboutButton.addActionListener(ev -> {
            new AboutBox(this).setVisible(true);
        });

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.insets = new Insets(2, 2, 2, 2);
        constraints.fill = GridBagConstraints.HORIZONTAL;

        optionButtonsPanel.add(statusLabel, constraints);

        constraints.weightx = 1;
        optionButtonsPanel.add(new JPanel(), constraints);

        constraints.weightx = 0;
        optionButtonsPanel.add(prefsButton, constraints);
        optionButtonsPanel.add(aboutButton, constraints);

        return optionButtonsPanel;
    }

    private JPanel initTimerPanel() {
        JPanel timerPanel = new JPanel();

        seekBar.addChangeListener(new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent ev) {
                try {
                    if (seekBar.getValueIsAdjusting()) {
                        soundHelix.seek(seekBar.getValue());
                    }
                } catch (NullPointerException x) {}
            }
        });

        timerPanel.setLayout(new GridBagLayout());

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.insets = new Insets(2, 2, 2, 2);

        constraints.gridx = 0;
        constraints.weightx = 0;
        timerPanel.add(timerLabel, constraints);

        constraints.gridx = 1;
        constraints.weightx = 1;
        timerPanel.add(seekBar, constraints);

        constraints.gridx = 2;
        constraints.weightx = 0;
        timerPanel.add(lengthLabel, constraints);

        return timerPanel;
    }

    private JPanel initInputsPanel() {
        JPanel inputsPanel = new JPanel();

        // Get list of styles for the combo box.
        String[] styleList = PathUtils.getStyles();

        styleChoice.addItem("Random");

        // Add styles to style choice combo box
        for (String style : styleList) {
            styleChoice.addItem(style);
        }

        // Set up layout
        inputsPanel.setLayout(new GridBagLayout());

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.insets = new Insets(2, 2, 2, 2);

        // Add song information
        constraints.gridy = 0;
        constraints.weightx = 0;
        inputsPanel.add(songNameLabel, constraints);
        constraints.weightx = 1;
        inputsPanel.add(songNameEntry, constraints);

        // Add style label
        constraints.gridy = 1;
        constraints.weightx = 0;
        inputsPanel.add(styleLabel, constraints);
        constraints.weightx = 1;
        inputsPanel.add(styleChoice, constraints);

        return inputsPanel;
    }

    private JPanel initPlayControlsPanel() {
        JPanel playControlsPanel = new JPanel();

        // Add tooltips to the buttons
        playButton.setToolTipText("Play");
        stopButton.setToolTipText("Stop");
        skipBackButton.setToolTipText("Skip back");
        skipForwardButton.setToolTipText("Skip forward");

        // Set up key bindings
        playButton.getInputMap(JButton.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_P,
                InputEvent.CTRL_DOWN_MASK), "play");

        playButton.getInputMap(JButton.WHEN_IN_FOCUSED_WINDOW)
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "play");

        playButton.getActionMap().put("play", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ev) {
                playSongFromInputs();
            }
        });

        playButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent ev) {
                playSongFromInputs();
            }
        });

        stopButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent ev) {
                soundHelix.stopSong();
            }
        });

        skipBackButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent ev) {
                soundHelix.seek(soundHelix.getSongTimeDelta(-30));
            }
        });

        skipForwardButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent ev) {
                soundHelix.seek(soundHelix.getSongTimeDelta(30));
            }
        });

        // Set up layout
        playControlsPanel.setLayout(new GridBagLayout());

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.insets = new Insets(2, 2, 2, 2);
        constraints.fill = GridBagConstraints.HORIZONTAL;

        playControlsPanel.add(skipBackButton, constraints);
        playControlsPanel.add(playButton, constraints);
        playControlsPanel.add(stopButton, constraints);
        playControlsPanel.add(skipForwardButton, constraints);

        return playControlsPanel;
    }

    private JPanel initHistoryPanel() {
        JPanel historyPanel = new JPanel();

        try {
            JLabel historyHeader = new JLabel("History");
            historyHeader.setFont(new Font("sans-serif", Font.BOLD, 18));

            historyTableModel = new HistoryTableModel();
            historyTable = new JTable(historyTableModel);

            historyTable.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
                    .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
                    "history-play");

            historyTable.getActionMap().put("history-play", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ev) {
                    playSongFromHistory();
                }
            });

            historyTable.addMouseListener(new MouseListener() {
                @Override
                public void mouseClicked(MouseEvent ev) {
                    if (ev.getButton() == MouseEvent.BUTTON1 &&
                            ev.getClickCount() == 2) {
                        playSongFromHistory();
                    }
                }

                @Override
                public void mouseEntered(MouseEvent ev) {}

                @Override
                public void mouseExited(MouseEvent ev) {}

                @Override
                public void mousePressed(MouseEvent ev) {}

                @Override
                public void mouseReleased(MouseEvent ev) {}
            });

            historyTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
            historyTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);

            historyTable.getColumn("Song name").setPreferredWidth(96);
            historyTable.getColumn("Style").setPreferredWidth(96);
            historyTable.getColumn("Length").setPreferredWidth(8);
            historyTable.getColumn("Time played").setPreferredWidth(96);

            historyScrollPane = new JScrollPane(historyTable);
            historyPanel.setLayout(new GridBagLayout());

            GridBagConstraints constraints = new GridBagConstraints();
            constraints.gridy = 0;
            constraints.insets = new Insets(2, 0, 2, 0);
            historyPanel.add(historyHeader, constraints);
            constraints.gridy = 1;
            constraints.fill = GridBagConstraints.BOTH;
            constraints.insets = new Insets(0, 0, 0, 0);
            constraints.weightx = 1;
            constraints.weighty = 1;
            historyPanel.add(historyScrollPane, constraints);
        } catch (IOException x) {
            JOptionPane.showMessageDialog(this,
                x.getMessage(),
                "I/O Error opening history.xml, disabling logging of songs.",
                JOptionPane.ERROR_MESSAGE
            );

            soundHelix.setLoggingSongs(false);
        } catch (NullPointerException | XMLStreamException x) {
            int replaceHistoryFile = JOptionPane.showConfirmDialog(this,
                "Malformed history.xml file, do you wish to overwrite it? " +
                "Choosing no will disable the logging of songs.",
                "I/O error",
                JOptionPane.YES_NO_OPTION
            );

            if (replaceHistoryFile == JOptionPane.NO_OPTION) {
                soundHelix.setLoggingSongs(false);
            }
        }

        return historyPanel;
    }

    public void setControlsEnabled(boolean enabled) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                playButton.setEnabled(enabled);
                stopButton.setEnabled(enabled);
                skipBackButton.setEnabled(enabled);
                skipForwardButton.setEnabled(enabled);

                if (enabled) {
                    playButton.requestFocusInWindow();
                }
            }
        });
    }

    // Calcaulates the length of the current song in seconds, the code is taken
    // from SoundHelix's MidiPlayer class.
    public synchronized int getCurrentSongLength() {
        try {
            return soundHelix.songLength() * 60000 /
                    (soundHelix.getTicksPerBeat() *
                    soundHelix.getMilliBPM());
        } catch (NullPointerException x) {
            return 0;
        }
    }

    private void playSongFromHistory() {
        int selectedRow = historyTable.getSelectedRow();

        if (selectedRow >= 0) {
            String title = (String)historyTableModel.getValueAt(selectedRow, 0);
            String style = (String)historyTableModel.getValueAt(selectedRow, 1);

            playSong(title, style);
        }
    }

    private void playSongFromInputs() {
        String style;
        if (styleChoice.getSelectedIndex() == 0) {
            String[] styles = PathUtils.getStyles();
            int styleIndex = Math.abs(new Random().nextInt() % styles.length);

            style = styles[styleIndex];
        } else {
            style = styleChoice.getSelectedItem().toString();
        }

        playSong(songNameEntry.getText(), style);
    }

    private synchronized void playSong(String title, String style) {
        soundHelix.nextSong.title = title;
        soundHelix.nextSong.style = style;
        soundHelix.nextSong.cued = true;
        soundHelix.playSong();
    }

    public void logSong(String songName, String styleName)
            throws IOException, XMLStreamException {
        int length = getCurrentSongLength();
        historyTableModel.logSong(songName, styleName, length);
    }

    public void clearHistoryModel() {
        historyTableModel.clear();
    }

    public void zeroTimer() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                timerLabel.setText("00:00");
                lengthLabel.setText("00:00");
                seekBar.setValue(0);
                seekBar.setMaximum(0);
            }
        });
    }

    public void refreshTimer() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                int position = soundHelix.getCurrentPos();

                timerLabel.setText(String.format("%02d:%02d",
                        position / 60, position % 60));

                seekBar.setValue(soundHelix.getCurrentTick());
            }
        });
    }

    public void refreshLength() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                int length = getCurrentSongLength();

                lengthLabel.setText(String.format("%02d:%02d",
                        length / 60, length % 60));

                seekBar.setMaximum(soundHelix.songLength());
            }
        });
    }

    public static void main(String[] args) {
        LongOpt[] opts = {
            new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'),
            new LongOpt("version", LongOpt.NO_ARGUMENT, null, 'v')
        };

        Getopt checker = new Getopt("MadHelix", args, "", opts);

        switch (checker.getopt()) {
            case 'h':
                System.out.println("Usage: java -jar " +
                        System.getProperty("java.class.path") + " [options]");

                System.out.println();
                System.out.println("Options:");
                System.out.println("        --help     Show this message");
                System.out.println("        --version  Show the version of " +
                        "MadHelix being executed.");
                System.out.println();

                break;

            case 'v':
                System.out.println("MadHelix version " +
                        VersionUtils.getMadHelixVersionAsString());

                System.out.println("Copyright (C) 2017-2019, 2023 " +
                        "UltrasonicMadness");
                System.out.println("Licensed under the GNU General Public " +
                        "License, version 3 only.");

                break;

            default:
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        new MadHelix();
                    }
                });
        }
    }
}
